#!/usr/bin/python2

import socket
import asyncore
from network import JSONConnection
import world
import weakref
import config
from collections import deque
import time
import logging

class BlockAlreadyAffected(Exception):
    pass

#clients are put here while they wait that all block servers are connected
#we use a weak set so that clients are removed automatically if they disconnect
waiting_clients = weakref.WeakSet()
#set of servers waiting for an update on this port
waiting_servers = weakref.WeakSet()

#clients waiting for the 10s timeout
waiting_timeout = deque()

#last time a notification was sent to as client
last_notification = 0.

blocks = dict((coord, None) for coord in world.block_list())

class IndexGenerator:
    def __init__(self):
        self.counter = 0
        
    def generate(self):
        self.counter += 1
        return self.counter
    
index_generator = IndexGenerator()

def flush_response():
    """
    Sends the block index to clients and servers once it is completed
    """

    if world.blocks_ready(blocks):
        logging.info("Flushing waiting clients")
        for client in waiting_clients:
            client.queue_client_response()
            
        waiting_clients.clear()
            
        for server in waiting_servers:
            server.send_server_response()
            
        waiting_servers.clear() 
    

class ConnectionHandler(JSONConnection, asyncore.dispatcher):
    def __init__(self, sock, addr):
        """
        handle block and client message
        """
        JSONConnection.__init__(self)
        asyncore.dispatcher.__init__(self, sock)
        
        self.remote_host, self.remote_port = addr
        
        self._commands = {'client_hello' : self.client_hello,
                                'block_hello' : self.block_hello}
        
    def add_block(self, coord, port):
        """
        adds a block server
        """
        
        world.check_bounds(*coord)
        
        if blocks[coord]:
            raise BlockAlreadyAffected()

        blocks[coord] = self.remote_host, port
        
        flush_response()
        
    def client_hello(self, msg):
        logging.info("Received client_hello")
        
        #store wether the client is a visitor or not
        #to know if we have to wait 10 seconds between two of them
        self.type = msg["type"]
        
        #TODO : add 10sec wait
        if world.blocks_ready(blocks):
            self.queue_client_response()
        else:
            waiting_clients.add(self)
            
    def queue_client_response(self):
        if self.type == "visitor":
            logging.debug("Appending client to timeout")
            waiting_timeout.appendleft(self)
        else:
            self.send_client_response()
            
    def send_client_response(self):
        logging.info("Sending client response")
        
        response = {"cmd" : "index_response"}
        response["id"] = index_generator.generate()
        response["start"] = world.start_loc
        response["blocks"] = world.format_blocks(blocks)
        self.write_json(response)
        
    def block_hello(self, msg):
        logging.info("Received block_hello")
        
        self.coord = tuple(msg["coord"])
        
        try:
            self.add_block(self.coord, msg["port"])
        except BlockAlreadyAffected:
            logging.warning("%s is already affected, closing connection", self.coord)
            self.close()
        
        if world.blocks_ready(blocks):
            self.send_server_response()
        else:
            waiting_servers.add(self)
        
    def send_server_response(self):
        response = {}
        response["cmd"] = "index_response"
        response["blocks"] = world.neighbours(blocks, self.coord)
        self.write_json(response)

    def handle_close(self):
        #TODO : handle block server disconnection
        self.close()
   
class Server(asyncore.dispatcher):
    def __init__(self, port, host='0.0.0.0'):
        asyncore.dispatcher.__init__(self)

        logging.info("Starting server at %s, %s", host, port)

        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(16)

    def handle_accept(self):
        pair = self.accept()
        if pair:
            sock, addr = pair
            ConnectionHandler(sock, addr)

if __name__ == "__main__":        
    logging.basicConfig(format="%(levelname)s INDEX %(asctime)-15s %(message)s" , level=logging.DEBUG)
    logging.info("Starting")
    
    _, port = config.get_index_server()
    server = Server(port)
    
    while True:
        #timeout every one second, to signal to the clients they can connect
        asyncore.loop(use_poll=True, timeout=1, count=1)
        
        #check wether 10 second has elapsed
        current_time = time.time()
        if current_time - last_notification > 10.:
            if len(waiting_timeout):
                #notify clients if they are waiting
                client = waiting_timeout.pop()
                client.send_client_response()
                last_notification = current_time
